BBOY_ALL_TRAVELABLES = {}

local function ontraveller(self, traveller)
    self.inst.replica.bboy_travelable:SetTraveller(traveller)
end

local default_dist_cost = 32
local max_hunger_cost = 75
local min_hunger_cost = 5
local sanity_cost_ratio = 0.2

local ownershiptag = "uid_private"

local Travelable = Class(function(self, inst)
    self.inst = inst
    self.inst:AddTag("bboy_travelable")

    self.traveller = nil
    self.destinations = {}
    self.travellers = {}

    self.onclosepopups = function(traveller) -- yay closures ~gj -- yay ~v2c
        if traveller == self.traveller then self:EndTravel() end
    end

    self.generatorfn = nil
    table.insert(BBOY_ALL_TRAVELABLES, self)
end, nil, { traveller = ontraveller })

local function DistToCost(dist)
    local cost_hunger = min_hunger_cost + dist / default_dist_cost
    cost_hunger = math.min(cost_hunger, max_hunger_cost)
    local cost_sanity = cost_hunger * sanity_cost_ratio
    if TheWorld.state.season == "winter" then
        cost_sanity = cost_sanity * 1.25
    elseif TheWorld.state.season == "summer" then
        cost_sanity = cost_sanity * 0.75
    end

    cost_hunger = math.ceil(cost_hunger)
    cost_sanity = math.ceil(cost_sanity)
    return cost_hunger, cost_sanity
end

local function ondoerarrive(who)
    who.sg:GoToState("bboy_jumpout")
end

function Travelable:ListDestination(traveller)
    self.destinations = {}

    for i, v in ipairs(BBOY_ALL_TRAVELABLES) do
        if not (v.ownership and v.inst:HasTag(ownershiptag) and
                traveller.userid ~= nil and
                not v.inst:HasTag("uid_" .. traveller.userid)) then
            table.insert(self.destinations, v.inst)
        end
    end

    table.sort(self.destinations, function(destA, destB)
        local writeA = destA.components.writeable
        local writeB = destB.components.writeable
        if writeA == nil or writeA:GetText() == nil or writeA:GetText() == "" then
            return false
        end
        if writeB == nil or writeB:GetText() == nil or writeB:GetText() == "" then
            return true
        end
        return string.lower(writeA:GetText()) < string.lower(writeB:GetText())
    end)

    self.totalsites = #self.destinations
    self.site = self.totalsites
end

function Travelable:MakeInfos()
    local infos = ""
    for i, destination in ipairs(self.destinations) do
        local name = destination.components.writeable and
            destination.components.writeable:GetText() or "~nil"
        local cost_hunger, cost_sanity = 0, 0
        if destination == self.inst then
            cost_hunger = -1
            cost_sanity = -1
        else
            local xi, yi, zi = self.inst.Transform:GetWorldPosition()
            local xf, yf, zf = destination.Transform:GetWorldPosition()
            -- entity may be removed
            if xi ~= nil and zi ~= nil and xf ~= nil and zf ~= nil then
                local dist = math.sqrt((xi - xf) ^ 2 + (zi - zf) ^ 2)
                cost_hunger, cost_sanity = DistToCost(dist)
            end
        end

        infos = infos .. (infos == "" and "" or "\n") .. i .. "\t" .. name ..
            "\t" .. cost_hunger .. "\t" .. cost_sanity
    end
    self.inst.replica.bboy_travelable:SetDestInfos(infos)
end

function Travelable:Teleport(traveller, destination)
    -- 当前坐标
    local xi, yi, zi = self.inst.Transform:GetWorldPosition()
    -- 传送坐标
    local xf, yf, zf = destination.Transform:GetWorldPosition()
    if traveller.Physics ~= nil then
        traveller.Physics:Teleport(xf - 1, 0, zf)
    else
        traveller.Transform:SetPosition(xf - 1, 0, zf)
    end

    -- follow
    if traveller.components.leader and
        traveller.components.leader.followers then
        for kf, vf in pairs(traveller.components.leader.followers) do
            if kf.Physics ~= nil then
                kf.Physics:Teleport(xf + 1, 0, zf)
            else
                kf.Transform:SetPosition(xf + 1, 0, zf)
            end
        end
    end

    local inventory = traveller.components.inventory
    if inventory then
        for ki, vi in pairs(inventory.itemslots) do
            if vi.components.leader and
                vi.components.leader.followers then
                for kif, vif in pairs(vi.components.leader.followers) do
                    if kif.Physics ~= nil then
                        kif.Physics:Teleport(xf, 0, zf + 1)
                    else
                        kif.Transform:SetPosition(xf, 0, zf + 1)
                    end
                end
            end
        end
    end

    local container = inventory:GetOverflowContainer()
    if container then
        for kb, vb in pairs(container.slots) do
            if vb.components.leader and
                vb.components.leader.followers then
                for kbf, vbf in pairs(vb.components.leader.followers) do
                    if kbf.Physics ~= nil then
                        kbf.Physics:Teleport(xf, 0, zf - 1)
                    else
                        kbf.Transform:SetPosition(xf, 0, zf - 1)
                    end
                end
            end
        end
    end

    -- heavy objects nearby
    if xi ~= nil and yi ~= nil and zi ~= nil then
        local heavy_insts = TheSim:FindEntities(
            xi, yi, zi, 3, { "heavy" })
        for i, v in ipairs(heavy_insts) do
            if v.components.equippable then
                if v.Physics ~= nil then
                    v.Physics:Teleport(xf + 1, 0, zf)
                else
                    v.Transform:SetPosition(xf + 1, 0, zf)
                end
            end
        end
    end

    -- 移除未命名的起点传送器,传送木牌的功能,我们用不到
    -- if self.inst.components.writeable and not self.inst.components.writeable:IsWritten() then
    --     self.inst:Remove()
    -- end
end

function Travelable:BeginTravel(traveller)
    local comment = self.inst.components.talker
    if not traveller then
        if comment then comment:Say("谁碰了我?") end
        return
    end
    local talk = traveller.components.talker

    if self.traveller then
        if comment then
            comment:Say("还没有轮到你.")
        elseif talk then
            talk:Say("还没有轮到我.")
        end
        return
    end

    local isintask = false
    for k, v in pairs(self.travellers) do
        if v == traveller then isintask = true end
    end

    if not self.traveltask or isintask then
        self.inst:StartUpdatingComponent(self)

        self:ListDestination(traveller)
        self:MakeInfos()
        self:CancelTravel(traveller)
        self.travellers = {}

        self.traveller = traveller
        self.inst:ListenForEvent("ms_closepopups", self.onclosepopups, traveller)
        self.inst:ListenForEvent("onremove", self.onclosepopups, traveller)

        if traveller.HUD ~= nil then
            self.screen = traveller.HUD:BBoy_ShowTravelScreen(self.inst)
        end
    else
        self:CancelTravel(traveller)
        self:Travel(traveller, self.site)
    end
end

function Travelable:Travel(traveller, index)
    local destination = self.destinations[index]

    if traveller and destination then
        self.site = index
        local comment = self.inst.components.talker -- 传送实体:井
        local talk = traveller.components.talker    -- 传送者

        -- Site information
        local desc = destination.components.writeable and
            destination.components.writeable:GetText()
        local description = desc and string.format('"%s"', desc) or
            "[未标注文字]"
        local information = ""
        local cost_hunger = min_hunger_cost
        local cost_sanity = 0
        -- 当前坐标
        local xi, yi, zi = self.inst.Transform:GetWorldPosition()
        -- 传送坐标
        local xf, yf, zf = destination.Transform:GetWorldPosition()
        -- entity may be removed
        if xi ~= nil and zi ~= nil and xf ~= nil and zf ~= nil then
            local dist = math.sqrt((xi - xf) ^ 2 + (zi - zf) ^ 2)
            cost_hunger, cost_sanity = DistToCost(dist)
        end

        if destination.components.bboy_travelable then
            -- 将传送者塞入到传送列表
            table.insert(self.travellers, traveller)

            -- information = string.format(
            --     "即将传送到: %s (%d/%d)\n[-]饱食度: %.0f\n[-]理智: %.0f",
            --     description, self.site, self.totalsites,
            --     cost_hunger, cost_sanity)
            -- if comment then
            --     comment:Say(information, 3)
            -- elseif talk then
            --     talk:Say(information, 3)
            -- end

            local travel_delay = 0

            self.traveltask = self.inst:DoTaskInTime(travel_delay, function()
                self.traveltask = nil
                local dest_pos_valid = xf ~= nil and zf ~= nil and TheWorld.Map:IsPassableAtPoint(xf, 0, zf)
                for k, who in pairs(self.travellers) do
                    if not destination:IsValid() or not dest_pos_valid then
                        if comment then
                            comment:Say("目的地不可用.")
                        elseif talk then
                            talk:Say("目的地不可用.")
                        end
                    elseif who == nil or
                        (who.components.health and
                            who.components.health:IsDead()) then
                        if comment then
                            comment:Say("无法传送[尸体]?")
                        end
                    elseif not (who:IsValid() and self.inst:IsValid() and who:IsNear(self.inst, 10)) then
                        print("目的地无效,或相距太远.")
                    elseif who.components.hunger and who.components.sanity then
                        who.components.hunger:DoDelta(-cost_hunger)
                        who.components.sanity:DoDelta(-cost_sanity)
                        who.sg:GoToState("bboy_jumpin", { teleporter = self.inst, teleporterexit = destination })
                        -- self:Teleport(who, destination)
                        who:DoTaskInTime(4, ondoerarrive)
                    else
                        if talk then
                            talk:Say("无法传送[可能是状态不好]?")
                        elseif comment then
                            comment:Say("无法传送[可能是状态不好]?")
                        end
                    end
                end
                self.travellers = {}
            end)
        elseif comment then
            comment:Say("目的地不可用[损坏了]?")
        elseif talk then
            talk:Say("目的地不可用[损坏了]?")
        end
    end
    self:EndTravel()
end

function Travelable:CancelTravel(traveller)
    if self.traveltask ~= nil then
        self.traveltask:Cancel()
        self.traveltask = nil
    end
    if self.traveltask1 ~= nil then
        self.traveltask1:Cancel()
        self.traveltask1 = nil
    end
    if self.traveltask2 ~= nil then
        self.traveltask2:Cancel()
        self.traveltask2 = nil
    end
    if self.traveltask3 ~= nil then
        self.traveltask3:Cancel()
        self.traveltask3 = nil
    end
    if self.traveltask4 ~= nil then
        self.traveltask4:Cancel()
        self.traveltask4 = nil
    end
    if self.traveltask5 ~= nil then
        self.traveltask5:Cancel()
        self.traveltask5 = nil
    end
end

function Travelable:EndTravel()
    if self.traveller ~= nil then
        self.inst:StopUpdatingComponent(self)

        if self.screen ~= nil then
            self.traveller.HUD:BBoy_CloseTravelScreen()
            self.screen = nil
        end

        self.inst:RemoveEventCallback("ms_closepopups", self.onclosepopups, self.traveller)
        self.inst:RemoveEventCallback("onremove", self.onclosepopups, self.traveller)

        if IsXB1() then
            if self.traveller:HasTag("player") and
                self.traveller:GetDisplayName() then
                local ClientObjs = TheNet:GetClientTable()
                if ClientObjs ~= nil and #ClientObjs > 0 then
                    for i, v in ipairs(ClientObjs) do
                        if self.traveller:GetDisplayName() == v.name then
                            self.netid = v.netid
                            break
                        end
                    end
                end
            end
        end

        -- self.traveller.sg:GoToState("bboy_jumpout")
        self.traveller = nil
    elseif self.screen ~= nil then
        -- Should not have screen with no traveller, but just in case...
        if self.screen.inst:IsValid() then self.screen:Kill() end
        self.screen = nil
    end
end

--------------------------------------------------------------------------
-- Check for auto-closing conditions
--------------------------------------------------------------------------

function Travelable:OnUpdate(dt)
    if self.traveller == nil then
        self.inst:StopUpdatingComponent(self)
    elseif not (self.traveller:IsNear(self.inst, 3) and
            CanEntitySeeTarget(self.traveller, self.inst)) then
        self:EndTravel()
    end
end

--------------------------------------------------------------------------

function Travelable:OnRemoveFromEntity()
    Travelable:OnRemoveEntity()
    self.inst:RemoveTag("bboy_travelable")
end

function Travelable:OnRemoveEntity()
    self:EndTravel()
    for i, v in ipairs(BBOY_ALL_TRAVELABLES) do
        if v == self then
            table.remove(BBOY_ALL_TRAVELABLES, i)
            break
        end
    end
end

return Travelable
